Chapter 12
MFC OLE Servers

by Gene Olafsen

In This Chapter

  Document Servers 450
  Server Types 451
  Document Server Design 452
  Building an Active DocumentServer 462
  Automation Servers 465
  IDispatch 467
  IDispatch-Derived Interface in ODL 467
  Calling Methods Through IDispatch 469
  Dispinterfaces Differ from Interfaces 471
  Accessing Automation Servers in C++ through IDispatch 471
  Dual Interfaces 472
  The Variant 473
  An Automation Server Using MFC 476

There are essentially two types of OLE server objects: automation servers and document servers. Likewise, the document server category can be further divided into in-place servers and active document servers. In-place editing server applications are those OLE server applications that MFC has supported for a number of releases and are compatible with the container applications that AppWizard creates. There are a few differences between in-place servers and active documents, which I will explore later in this chapter.

Active documents, from a framework perspective, use classes that derive from the in-place object server classes. This derivation offers a set of classes that tailor the more general OLE server base classes into a new breed of base classes for active document support. Specifically, this change involves using a new in-place frame class and a different server item class.

Document Servers

Document servers are applications that you probably use every day. Both Microsoft Word and Excel are perfect examples of OLE document servers. In Figure 12.1, an Excel spreadsheet is shown in-place active in a Word document. You can see how convenient it is to edit a document’s data using the application in which it was created.


Figure 12.1  Excel in-place active inside Word.

Server Types

You have a number of options when using AppWizard to create your document server application, as shown in Figure 12.2.


Figure 12.2  AppWizard document server options (step 3).

Full Server

A full server is the real deal. You can execute a full server either as a standalone application or from within a container. Because the server can act as a standalone application, it can load, store, and create data files. A container application can then either instantiate full server objects to create embedded data items, or activate the server by linking to external data files.

Active Document

An active document server derives from OLE server technology. From an MFC coding view, there is little that differentiates an active document from a more traditional OLE server object other than a couple of classes and the addition of some command message routing.

The differences between active documents and OLE server objects are purported to be advantages. The following is a list of differences (read advantages) of active documents over embedding servers:

  Active documents always display in the entire client area of the container application. They are always in-place active. They cannot display themselves in a small rectangular region of the document, whose border is identified by a hatched border rectangle—as is a familiar representation by common embedding servers.
  The container may route menu commands to the active document server. Thus, from a user interface perspective, this seamless integration of container and server application defines an inextricable joining.
  You can view active documents in a Web browser, part of the Internet-everywhere approach. Essentially, this point means that the Web browser is not just for viewing pages on the Internet but is a viewer (and editor) of all file types, regardless of origin.

AppWizard offers options to implement any server as an active document server and any container as an active document container.

Container/Server

A container-server application is one that can function either as a standalone container program, in which case you can link or embed other document servers, or embedded as a document server in a different container. In fact, your server can continue to act as a container while it is performing server functions for a container. It sounds a bit more complex than it really is. Both Microsoft Excel and Word are container-server applications. As container applications, either can embed the other performing in its document server role. In the most extreme scenario, a document server retains its container functionality; thus, it can offer document viewing and activation facilities.

Mini-Server

A mini-server offers a subset of full server functionality. The most differentiating aspect of a mini-server is that it cannot run as a standalone application. Because the server can never execute in the same sense as a normal application, it can load only as store data through the container application. Mini-servers can only be embedded in a server, as linking requires the document data to exist outside the container’s document—something that you have already seen is not permitted.

Document Server Design

When creating a document server, there is no better place to start than with AppWizard (see Figure 12.3). The project name for the application is activedocserv. You will see how to create a simple active document server and then host it in Internet Explorer.


Figure 12.3  Starting the activedocserv project.

OLE Documents

The concept of OLE Documents can be found in the initial release of OLE. After all, the words “Object Linking and Embedding” describe a document-centric technology. The early implementation of OLE documents was very crude, generally requiring an object packager utility to combine the elements of the document. Today, OLE document construction occurs seamlessly in word processors and spreadsheets, while viewing such documents can even occur in Web browsers.

Servers and the Document/View Architecture

MFC’s implementation of embedding servers leverages the document/view architecture to manage multiple container support. Servers are very complicated animals, requiring operation under a number of circumstances. One of the most taxing of all situations is a document server object simultaneously supporting two containers. Your server will handle this situation differently depending on the options that you select in AppWizard when constructing the server application, as shown in Figure 12.4. An OLE server that is an MDI application simply “opens” a new window for each document that the server requires to support. Hence, a single instance of the executable can handle multiple container requests or multiple document instantiations in a single container. An SDI server or mini-server doesn’t contain the necessary framework wiring for a single application instance to share across multiple containers. The solution to this problem is multiple application instances.


Figure 12.4  Document architecture selection.

The activedocserv application will only be an SDI application. As part of navigating through the wizard, you should also do the following:

  Take the default for step 2, No Database Support.
  For step 3, select Full-Server and Active Document Server.



Registration

The Windows operating systems, and more specifically the OLE DLLs, make extensive use of the Registry as a directory of file locations, service mappings, preference settings, and object registration. All OLE servers must perform a registration process that places the filespec location, among other properties, in the Registry for access by container applications. The following lines illustrate the Registry entries for a typical OLE server object:

[HKEY_CLASSES_ROOT\CLSID\{723B4D4D-B8AA-11D2-8FAF-00105A5D8D6C}]
“Typical Document”
\AuxUserType
\AuxUserType\2=“Typical”
\AuxUserType\3=“TypicalServer”
\DefaultIcon]=“F:\\vctemp\\TYPICA-1\\Debug\\TYPICA-1.EXE,1”
\InprocHandler32=“ole32.dll”
\Insertable=“”
\LocalServer32=“F:\\vctemp\\TYPICA-1\\Debug\\TYPICA-1.EXE”
\MiscStatus=“32”
\ProgID=“TypicalServer.Document”
\Verb
\Verb\0=“&Edit,0,2”
\Verb\1=“&Open,0,2”

The entries identify the following basic information. The AuxUserType entries specify a short display name of the server. The DefaultIcon entry provides the path of the file containing the resource and index of the icon to display by default. The InprocHandler32 entry defaults to using the OLE libraries as the default handler. The name and location of the server is provided by LocalServer32, whereas MiscStatus offers flags that interest IOleObject::GetMiscStatus. The ProgID is, for all intents and purposes, a human-readable version of the object’s GUID. Finally, the Verb entry specifies the standard verbs that the object supports.

You will undoubtedly prefer your servers to register themselves than have to make users of your objects perform this step manually. Visual C++ will automatically create a .REG file that defines the registration entries for your object. Generally the setup program that you ship with your server will include this .REG file in the distribution set and update the installer’s machine accordingly. For mini-servers, either your install program must insert these Registry entries or the user must import the .REG file with RegEdit. Mini-servers cannot perform self-registration simply because they cannot be run in a standalone fashion. MFC supports self-registration of OLE servers with a call to the static member function COleTemplateServer::RegisterAll() in your server’s InitInstance function.

The fourth step of the AppWizard enables you to select miscellaneous options including whether you want to support a status bar, printing, and so on. You will not be able to progress past this step without defining a file extension for your active document server. Selecting the Advanced button displays the Advanced Options dialog, as shown in Figure 12.5.


Figure 12.5  The Advanced Options dialog.

Many of the strings that you can define in this dialog box are stored in the Registry. You can also use the second tab in this dialog to customize various frame window attributes.

Accept the default settings for the last AppWizard steps and return to the Visual C++ editor.

Command-Line Arguments

A full server can execute both under the context of a container application and as a standalone Windows application. Your program identifies the circumstances under which it is executing by examining the command-line arguments that are passed to it on initialization.

The following lines will appear in your InitInstance function, courtesy of AppWizard:

    // Parse command line for standard shell commands, DDE, file open
    CCommandLineInfo cmdInfo;
    ParseCommandLine(cmdInfo);

    // Check to see if launched as OLE server
    if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
    {
        // Register all OLE server (factories) as running.
        //This enables the
        //  OLE libraries to create objects from other applications.
        COleTemplateServer::RegisterAll();

        // Application was run with /Embedding or /Automation.
        // Don’t show the main window in this case.

        return TRUE;
    }

A CCommandLineInfo object is passed to your application object’s ParseCommandLine function, which handles the command-line arguments and flags. A check is performed to see if your application is running as a request from an embedding container or as an automation server for an automation controller. Your server will register all of its class factories and return without creating a main window.

Server Item/Client Item

The relationship between container and server is a complex one. It should not be difficult for you to believe that in order for this relationship to function, a container must manage a list of servers that it links or embeds, whereas a server must manage a list of containers that it is serving. The CDocItem class offers a bridge between MFC-based containers and document servers. The classes COleClientItem and COleServerItem both derive from CDocItem, whose only methods are GetDocument and a virtual overridable IsBlank method. The COleClientItem class represents a server object, and the container maintains a collection of these objects—one for each object it embeds or links. COleServerItem class objects are maintained by document servers and either represent a whole document, in the case of embedded items, or part of a document, for linked items.

In-Place Frames

The in-place frame window contains the client area that your server “draws on” when it is active. For traditional embedding servers, a hatched border is drawn around your in-place frame when the server is in-place active in a container. The additional effect added to the border helps you to more easily identify the boundary of your server with respect to the rest of the container’s document. Active document servers differ both by the class that they derive from offering in-place frame window support, and by the fact that active documents take over the total client area of a container when they are active.

A major function of the in-place frame class is to manage toolbar creation. From a user interface perspective, your server’s toolbars will be active in the container’s space, outside your in-place frame, and will appear and function as if they are part of the container. AppWizard will create a toolbar that includes buttons for cut, paste, copy, and help by default. The OnCreateControlBars method will be overridden, and the following code is generated:

BOOL CInPlaceFrame::OnCreateControlBars(CFrameWnd* pWndFrame, \
                                        CFrameWnd* pWndDoc)
{
    // Remove this if you use pWndDoc
    UNREFERENCED_PARAMETER(pWndDoc);
    // Set owner to this window, so messages
    // are delivered to correct app
    m_wndToolBar.SetOwner(this);

    // Create toolbar on client’s frame window
    if (!m_wndToolBar.CreateEx(pWndFrame, \
         TBSTYLE_FLAT,WS_CHILD | WS_VISIBLE | CBRS_TOP
         | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | \
         CBRS_SIZE_DYNAMIC) ||
        !m_wndToolBar.LoadToolBar(IDR_TYPICATYPE_SRVR_IP))
    {
        TRACE0(“Failed to create toolbar\n”);
        return FALSE;
    }

    // TODO: Delete these three lines if you don’t want the toolbar to
    //  be dockable
    m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
    pWndFrame->EnableDocking(CBRS_ALIGN_ANY);
    pWndFrame->DockControlBar(&m_wndToolBar);

    return TRUE;
}

Adding additional toolbars requires you to perform the following steps:

1.  Create a new toolbar resource.
2.  Define a CToolBar member variable in your in-place frame declaration.
3.  Call the SetOwner method on the CToolBar class with the in-place frame as its parent window.
4.  Create the toolbar with the CreateEx method and load the toolbar specifying the resource ID.
5.  Finally, decide if you want your toolbar to be dockable. If you want the toolbar to dock in the container application, call the EnableDocking method specifying the sides of the container to which the toolbar can dock.

Active Documents

There are a number of differences between the traditional OLE embedding document server and active document servers. These differences mostly involve the server’s presentation to the user.



CDocObjectServerItem

The CDocObjectServerItem class derives from COleServerItem and is the class that active document servers use to help manage document state. CDocObjectServerItem does not add additional methods to its base class; however, it modifies the behavior of three virtual functions: OnHide, OnOpen, and OnShow.

OnOpen and OnShow behave similarly, defaulting to the base-class implementation if the server item is not a DocObject.

void CDocObjectServerItem::OnOpen()      // or OnShow
{
   COleServerDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   if (pDoc->IsDocObject())
      pDoc->ActivateDocObject();
   else
      COleServerItem::OnOpen();    // or OnShow
}

OnHide behaves differently, throwing an exception if an attempt is made to hide the object. Because active documents reside in the full client area of a container, it does not make sense to hide the server’s view.

void CDocObjectServerItem::OnHide()
{
   COleServerDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   if (pDoc->IsDocObject())
      AfxThrowOleException(OLEOBJ_E_INVALIDVERB);
   else
      COleServerItem::OnHide();
}

COleDocIPFrameWnd

No reference material is available from Microsoft for one of the base classes enabling active document servers. The COleDocIPFrameWnd class is used in place of the COleIPFrameWnd for active document servers and, not surprisingly, derives from COleIPFrameWnd. Here is the class declaration:

class COleDocIPFrameWnd : public COleIPFrameWnd
{
    DECLARE_DYNCREATE(COleDocIPFrameWnd)
// Constructors
public:
    COleDocIPFrameWnd();
// Attributes
public:
// Operations
public:
// Overridables
protected:
// Implementation
public:
    virtual -COleDocIPFrameWnd();
#ifdef _DEBUG
    virtual void AssertValid() const;
    virtual void Dump(CDumpContext& dc) const;
#endif
    // Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(COleDocIPFrameWnd)
    //}}AFX_VIRTUAL
protected:
    virtual void OnRequestPositionChange(LPCRECT lpRect);
    virtual void RecalcLayout(BOOL bNotify = TRUE);
    // Menu Merging support
    HMENU m_hMenuHelpPopup;
    virtual BOOL BuildSharedMenu();
    virtual void DestroySharedMenu();
    // Generated message map functions
    //{{AFX_MSG(COleDocIPFrameWnd)
        // NOTE - the ClassWizard will add and remove
        //        member functions here.
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

Although this class doesn’t appear in any Microsoft documentation, some of the functions that it implements are themselves undocumented virtual functions of COleIPFrameWnd. These functions can be found in the class declaration and are commented with an “Advanced:” prefix, as shown following:

// Advanced: in-place activation virtual implementation
// virtual BOOL BuildSharedMenu();
    virtual void DestroySharedMenu();
    virtual HMENU GetInPlaceMenu();

    // Advanced: possible override to change in-place sizing behavior
    virtual void OnRequestPositionChange(LPCRECT lpRect);

If you are interested in what separates active document in-place frame behavior from traditional document servers, you have to look no further than the code that the MFC framework offers as implementation.

GetInPlaceMenu

The GetInPlaceMenu function simply returns the in-place menu associated with the active document’s document template:

HMENU COleIPFrameWnd::GetInPlaceMenu()
{
    // get active document associated with this frame window
    CDocument* pDoc = GetActiveDocument();
    ASSERT_VALID(pDoc);

    // get in-place menu from the doc template
    CDocTemplate* pTemplate = pDoc->GetDocTemplate();
    ASSERT_VALID(pTemplate);
    return pTemplate->m_hMenuInPlaceServer;
}

OnRequestPositionChange

Server objects that want to change their size issue IOleInPlaceSite::OnPosRectChange calls. The container application responds with IOleInPlaceObject::SetObjectRects to position the in-place window. The following MFC code returns immediately if it identifies the server object as one that is an active document; otherwise, the request is passed to the server document. The IsDocObject() function determines whether the server object is an active document with the presence of a DocObject key under the server’s CLSID entry.

void COleDocIPFrameWnd::OnRequestPositionChange(LPCRECT lpRect)
{
    COleServerDoc* pDoc = (COleServerDoc*) GetActiveDocument();
    ASSERT_VALID(pDoc);
    ASSERT_KINDOF(COleServerDoc, pDoc);
    // DocObjects don’t need to generate OnPosRectChange calls, so you
    // just return if this is a DocObject.
    if (pDoc->IsDocObject())
        return;
    // The default behavior is to not affect the extent during the
    //  call to RequestPositionChange.  This results in consistent
    //  scaling behavior.
    pDoc->RequestPositionChange(lpRect);
}

BuildSharedMenu

The BuildSharedMenu function acquires the in-place menu and merges it with the container’s menu. A menu descriptor is then stored, which is used when dispatching menu messages and commands by OLE:

BOOL COleDocIPFrameWnd::BuildSharedMenu()
{
    HMENU hMenu = GetInPlaceMenu();

    // create shared menu
    ASSERT(m_hSharedMenu == NULL);
    if ((m_hSharedMenu = ::CreateMenu()) == NULL)
        return FALSE;

    // start out by getting menu from container
    memset(&m_menuWidths, 0, sizeof m_menuWidths);
    if (m_lpFrame->InsertMenus(m_hSharedMenu, &m_menuWidths) \
        != NOERROR)
    {
        ::DestroyMenu(m_hSharedMenu);
        m_hSharedMenu = NULL;
        return FALSE;
    }

#ifdef _DEBUG
    // container shouldn’t touch these
    ASSERT(m_menuWidths.width[1] == 0);
    ASSERT(m_menuWidths.width[3] == 0);

    // container shouldn’t touch this unless you’re
    // working with a DocObject
    COleServerDoc* pDoc = (COleServerDoc*) GetActiveDocument();
    ASSERT_VALID(pDoc);
    ASSERT_KINDOF(COleServerDoc, pDoc);
    if (!pDoc->IsDocObject())
        ASSERT(m_menuWidths.width[5] == 0);
#endif

    // only copy the popups if there is a menu loaded
    if (hMenu == NULL)
        return TRUE;

    // insert our menu popups amongst the container menus
    m_hMenuHelpPopup = AfxMergeMenus(m_hSharedMenu, hMenu,
        &m_menuWidths.width[0], 1, TRUE);

    // finally create the special OLE menu descriptor
    m_hOleMenu = ::OleCreateMenuDescriptor(m_hSharedMenu, \
                                           &m_menuWidths);

    return m_hOleMenu != NULL;
}

DestroySharedMenu()

The DestroySharedMenu function behaves in no particularly interesting manner and simply deletes any objects created in BuildSharedMenu and returns the menu to its original state.

Registration

There are a few registration differences that you should be aware of between active document servers and traditional OLE document servers. AppWizard will place the following lines in your program’s InitInstance function:

// When a server application is launched standalone,
// it is a good idea
//  to update the system registry in case it has been damaged.
    m_server.UpdateRegistry(OAT_DOC_OBJECT_SERVER);

If you are building an active document server, the UpdateRegistry function will appear as shown previously; otherwise, the constant will be OAT_INPLACE_SERVER. Active document servers also require an #include <afxdocob.h> definition in your stdafx.h file.



Building an Active Document Server

You can create an active document server with a minimum of work using the Visual Studio AppWizard. The wizard will lead you through a number of steps from which you select the type of server support you require, and it generates skeletal code into which you “plug” your server-specific functionality.

Persistence

Document servers can support file operations in the same manner as traditional Windows applications. You can override the Serialize function and perform the appropriate read and write operations. In the case of this sample document server, a single character is written to and read from a file.

void CActivedocservDoc::Serialize(CArchive& ar)
{
    CString sData;

    if (ar.IsStoring())
    {
        char buf[10];
        itoa(dwData, buf, 10);
        sData = buf;
        ar << sData;
    }
    else
    {
        ar >> sData;
        dwData = -atoi(sData);
    }
}

The member variable dwData is converted to a string and stored in a CString variable whose name is sData. The numeric value is written and read from the stream using CString because it derives from CObject and thus implements serialization. Notice that when the data value is read from the file, the member variable is set to a negative value. In this manner, the server knows that the value was read from a file and should not be changed.

The document also contains two member functions for member data access. SetData is the mutator function, setting the dwData value, and GetData is the accessor function for dwData.

void CActivedocservDoc::SetData(int dw)
{
    dwData = dw;
}

int CActivedocservDoc::GetData()
{
    return dwData;
}

Rendering the View

You draw in the client area of your server’s view as you would in any other application. In this case, the system’s tick-count value is obtained and the remainder after a division operation is used to obtain a small number. Using this number as the bounds for a loop, a number of lines are drawn out from each corner of the client window (see Figure 12.6).


Figure 12.6  The activedocserv application executing standalone.

Changing the size of the view, by dragging a corner or side of the window, will cause a repaint and a different number of lines to appear.

void CActivedocservView::OnDraw(CDC* pDC)
{
    CActivedocservDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    pDC->SelectObject(::GetStockObject(BLACK_PEN));
    RECT rect;
    GetClientRect(&rect);
    int dw = 0;
    dw = GetDocument()->GetData();
    if (dw >= 0) {
        dw = (int)(::GetTickCount() % 3);
        GetDocument()->SetData(dw);
    }
    else
        dw = -dw;
    for (int i=0; i< (dw+1); ++i)
    {
        pDC->MoveTo(0,0);
        pDC->LineTo(100+(10*i),100);
        pDC->MoveTo(rect.right,0);
        pDC->LineTo(rect.right-100-(10*i), 100);
        pDC->MoveTo(0,rect.bottom);
        pDC->LineTo(100+(10*i), rect.bottom-100);
        pDC->MoveTo(rect.right,rect.bottom);
        pDC->LineTo(rect.right-100-(10*i), rect.bottom-100);
    }
}

Running this program as a standalone application, you can choose to save your document with the number of lines that it displays. Of course, this is an almost totally useless server and can never be considered important for any purpose but this demonstration. If you display the document you saved, either in Internet Explorer or Microsoft Office Binder, your view will redisplay with the number of lines that you saved, as shown in Figure 12.7.


Figure 12.7  The activedocserv application as an active document server inside Internet Explorer container.

You will see that if the document’s member variable dwData contains a negative value, the view code ignores calculating a line count and uses the absolute value of this variable as the bounds for a counter.

Automation Servers

OLE automation (or as it is now simply referred to, automation) has been one of the best-exploited technologies of the early COM/OLE offerings. The idea behind automation is to expose an application’s functionality through a standard, language-independent, programmable interface. Microsoft has continued to embrace automation by exposing functionality through programmable interfaces for almost every program it offers. Additionally, it has extended its new Web technology languages (J++ and VBScript) the capability to easily program automation servers.

Automation can be summed up as offering the following features:

  Provides a programming language-independent model for exposing application functionality.
  Defines standard datatypes and provides the necessary marshaling for parameters.
  Defines a standard mechanism for a controller to explore and identify interface methods and properties. This information is available from the automation server’s type library.

Automation defines objects that expose the IDispatch interface as servers, more specifically, automation servers. However, clients are referred to as automation controllers. The word “controller” is used because these clients are generally languages, such as Visual Basic, Java, VBScript, or even C++. In fact, Microsoft Office provides a language called VBScript that is an automation controller and can “program” any automation server.

Defining interfaces for automation servers is not much different from defining interfaces for ordinary COM interfaces. The primary difference between the interfaces you have dealt with in COM servers to this point have been derived from IUnknown. Automation server interfaces must derive from IDispatch. These interfaces are commonly referred to as dispinterfaces.

You define interfaces with either Interface Definition Language (IDL) or Object Definition Language (ODL). Actually there is some history to explain, because it doesn’t seem necessary to have two languages that perform the same operation. Actually, IDL is a language whose roots are in Remote Procedure Call (RPC) technology. COM and Distributed COM (DCOM) have their roots in the interface-modeling scheme and Globally Unique Identifier (GUID) concept that are part of RPC definition. Microsoft made its changes to this standard definition, changing the file extension to .ODL and providing a utility MkTypLib to create type library information. Over the past few years, the IDL specification has come to include the ODL extensions, and now the MIDL utility can perform the functions that MkTypeLib does. For historical purposes only, MFC and Visual C++’s ClassWizard still generates .ODL files.

The results of ODL/IDL compilation are proxy and stub files, which aid in marshaling parameters, and a type library. For a COM client to “call” methods on an interface that resides on a COM server, it must make calls through a proxy. Information contained in header files provides the client developer with strict compile-time checking of method names and argument datatype and ordering. If the checking is successfully implemented, the client can then proceed to interact with the server.

At issue are those languages that don’t support header file definitions of interfaces, thus ruling out almost every language on the planet except C/C++. Automation solves this problem with the capability to discover at runtime the methods, parameters, and return values that an interface exposes. Runtime identification has become a hot topic with the advent of Java. Java’s reflection API allows a programmer to identify similar attributes as exposed by Java objects. This mechanism, along with a standard method and property naming convention, forms the heart of the Javabean specification.

IDispatch

The IDispatch interface, as every COM interface must, derives from IUnknown. In addition to the three IUnknown methods (QueryInterface, AddRef, and Release), IDispatch defines four additional methods, which are summarized in the following sections.



GetIDsOfNames

The GetIDsOfNames method is used to retrieve the DISPID value of a dispinterface method given its name. The name is provided in the form of a “human readable” string.

This function can translate more than one method name to its associated DISPID at a single time because it accepts an array of names and returns an array of dispatch IDs. It is always advisable to use functions such as this to their fullest extent because OLE calls (round trips) are very “expensive” in terms of processor time and network time. So you would be wise to process method names in groups wherever possible instead of calling this function for each name separately.

GetTypeInfo

The GetTypeInfo method returns a pointer to an ITypeInfo object. The methods of the ITypeInfo interface describe the methods, properties, and arguments of the dispinterface on which it is called.

GetTypeInfoCount

The GetTypeInfoCount method returns the number of type information interfaces that are provided by the dispinterface on which it is called. Simply put, this method returns either a one or a zero. If a one is returned, GetTypeInfo will return a useful interface; otherwise, a value of zero indicates that type information is not available.

Invoke

The Invoke method is used to call the methods of the dispinterface. This method is the heart of an automation object with arguments that identify the DISP of the method being called, the associated argument list, and space for return values.

IDispatch-Derived Interface in ODL

Automation servers derive from the IDispatch interface. Thus in addition to the properties and methods that you define on your object, the interface must also support the three methods of IUnknown and the four methods of IDispatch. The interface definition for IDispatch follows. You can see that as with custom interface methods, this definition provides argument direction and HRESULT return value information.

[
    object,
    uuid(00020400-0000-0000-C000-000000000046),
    pointer_default(unique)
]
interface IDispatch : IUnknown
{
    typedef [unique] IDispatch * LPDISPATCH;

    HRESULT GetTypeInfoCount(
                [out] UINT * pctinfo
            );

    HRESULT GetTypeInfo(
                [in] UINT iTInfo,
                [in] LCID lcid,
                [out] ITypeInfo ** ppTInfo
            );

    HRESULT GetIDsOfNames(
                [in] REFIID riid,
                [in, size_is(cNames)] LPOLESTR * rgszNames,
                [in] UINT cNames,
                [in] LCID lcid,
                [out, size_is(cNames)] DISPID * rgDispId
            );

    [local]
    HRESULT Invoke(
                [in] DISPID dispIdMember,
                [in] REFIID riid,
                [in] LCID lcid,
                [in] WORD wFlags,
                [in, out] DISPPARAMS * pDispParams,
                [out] VARIANT * pVarResult,
                [out] EXCEPINFO * pExcepInfo,
                [out] UINT * puArgErr
            );

    [call_as(Invoke)]
    HRESULT RemoteInvoke(
                [in] DISPID dispIdMember,
                [in] REFIID riid,
                [in] LCID lcid,
                [in] DWORD dwFlags,
                [in] DISPPARAMS * pDispParams,
                [out] VARIANT * pVarResult,
                [out] EXCEPINFO * pExcepInfo,
                [out] UINT * pArgErr,
                [in] UINT cVarRef,
                [in, size_is(cVarRef)] UINT * rgVarRefIdx,
                [in, out, size_is(cVarRef)] VARIANTARG * rgVarRef
            );
}

The Visual C++ compiler’s AppWizard and ClassWizard mask the interface derivation in ODL behind a dispinterface statement, but you shouldn’t forget that it is there.

Calling Methods Through IDispatch

The IDispatch interface offers all of the methods necessary for an automation controller to identify and initiate methods on the server and get/set properties.

GetIDsOfNames

You might already know, or be shocked to learn, that the IDispatch method GetIDsOfNames returns a DISPID given a method’s name. Okay, maybe “shocked” is a bit of a strong word, but the fact is that the Invoke method doesn’t accept the name of the method to call—instead it relies on DISPIDs to identify methods and properties. DISPID is short for dispatch id, and it is essentially boils down to the fact that each method and property is assigned a unique integer identifier. Think of it as a handle to a method:

HRESULT GetIDsOfNames(REFIID riid, LPOLESTR*, UINT cNames, \
                      LCID lcid, DISPID* rgdispid)

The GetIDsOfNames function requires five arguments. The first four parameters are used to specify the method or property of interest, and the DISPID is returned as the fifth.

REFIID riid A reserved parameter and must always be IID_NULL.
LPOLESTR* Identifies an array of names that are to be mapped.
UINT cNames The number of entries in the name array.
LCID lcid The locale context identifier, normally LOCALE_SYSTEM_DEFAULT.
DISPID* rgdispid Storage space for the IDs of the method names when the function returns.

An automation client can use GetIDsOfNames to acquire the DISPIDs of each method and property and keep these values cached for subsequent calls to Invoke. It is important to remember that each call to a COM object can be expensive if the server resides on a different machine. Therefore it is important to carefully consider the performance penalty for each call you must make. One of the tenets of COM interface design is that an interface is “immutable.” That is, the interface should never change after it is “published” (made available by a server). If this “rule” is followed, there is no reason that an automation client could not query a server for the DISPIDs of each method and property it intends to use the first time the application is started and store the values away in a file or database for subsequent use. Additional invocations of the client could retrieve the values from this permanent store, instead of making a number of GetIDsOfNames calls.

Type Information Methods

Before I move on to describing the Invoke method, arguably the most important of the IDispatch methods, I should discuss GetTypeInfo and GetTypeInfoCount. A little history is in order to understand their names and their purpose. When OLE2 was introduced, developers had only one book to turn to for help with the seemingly mind-boggling technology. The book was Inside OLE and its author, Kraig Brockschmidt, eclipsed Petzold in the hearts and minds of any readers wanting to “do COM.” Brockschmidt goes through an extensive discussion and presents a number of examples that describe the process of creating a type library, without the use of MkTypeLib. Because the OLE technology was so new, it wasn’t apparent where type libraries would be used—except for automation. Today it is common to generate type information for almost every server that you create.



Invoke

The final method of IDispatch to discuss is Invoke:

HRESULT Invoke( DISPID dispidMember, REFIID riid, LCID lcid, \
WORD wFlags, DISPPARMS* pdispparams, VARIANT* pvarResult, \
EXCEPINFO* pexcepinfo, UINT* puArgErr )

Invoke takes a number of arguments, and its purpose is to call automation methods and get or set automation properties.

Table 12.1 Arguments for the Invoke Method

Argument Description

DISPID dispidMember Identifies the method or property to invoke.
REFIID riid The riid argument must be IID_NULL.
LCID lcid Specifies the locale context. Unless you are supporting multiple objects, this can be LOCALE_SYSTEM_DEFAULT.
WORD wFlags Identifies the action to take by Invoke.
DISPATCH_PROPERTYGET Retrieve a property value.
DISPATCH_PROPERTYPUT Put a property value.
DISPATCH_PROPERTYPUTREF Put a property value by reference.
DISPATCH_METHOD Method invocation.
DISPPARMS* pdispparams A pointer to a DISPPARAMS structures containing the arguments.
VARIANT* pvarResult A pointer to a variant that will store the result.
EXCEPINFO* pexcepinfo If the function returns DISP_E_EXCEPTION, this structure will contain the exception information.
UINT* puArgErr If the function returns DISP_E_TYPEMISMATCH or DISP_E_PARAMNOTFOUND, this is the index of the first argument whose format is in question.

Dispinterfaces Differ from Interfaces

The dispinterface certainly broadens the number of languages that can take advantage of COM object servers; however, there are differences with non–IDispatch-derived interfaces that should be noted:

  Methods can use only OLE automation-compliant datatypes when specifying parameters in IDL.
  The keyword dispinterface supports the concept of methods and properties in its definition block.

Accessing Automation Servers in C++ Through IDispatch

This is probably a good time to show how an automation server is exercised by a C++ controller application. The steps taken in this example illustrate many of the concepts that were introduced in the previous section. These steps do not use the MFC framework; they are just a quick review to show you how a client uses the IDispatch methods to exercise a server.

The IDispatch pointer must be obtained from the server, as follows:

hresult = punk->QueryInterface(IID_IDispatch,\
(void FAR * FAR *)&pIDispatch);
    if (FAILED(hresult)){
        MessageBox(NULL,“Could not get IDispatch”,“Client”,MB_OK);
        return FALSE;
        }
    punk->Release();

Next the DispID of the method which you want to invoke must be obtained:

char    FAR* szDispName = “MyProperty”;
hresult = pIDispatch->GetIDsOfNames(IID_NULL, &szDispName,1,\
 LOCALE_SYSTEM_DEFAULT,&dispid);

The variant argument values must be configured:

VARIANTARG  vargResult;
VariantInit(&vargResult);
hresult = pIDispatch->Invoke(dispid,IID_NULL,
     LOCALE_SYSTEM_DEFAULT,DISPATCH_PROPERTYGET,&disp,&vargResult,\
NULL,NULL);

The property returned is a string value and the result is displayed in a message box:

LPSTR   lpszRetString;

    VariantChangeType(lpv,lpv,0,VT_BSTR);
    lpszRetString = (LPSTR)V_BSTR(lpv);
    MessageBox(NULL,lpszRetString,“String Returned”,MB_OK);

Dual Interfaces

Although dispinterfaces can be ideal for languages that make it difficult, if not impossible, to deal with the pointers of a vtable of an ordinary interface, the overhead involved with accessing automation servers in C++ might be unacceptable. Thankfully there is quite a simple solution to this problem, and it is called dual interfaces.

Dual interfaces are derived from IDispatch, just like dispinterfaces; however, they also expose their vtable. This is really quite convenient because it allows a single server to provide both implementations in the same physical DLL or EXE package (if an automation can be envisioned as residing in the physical universe) and allows the client to select the method with which it is most comfortable accessing the component. After an automation server is installed on a machine, a VBScript program can access the server’s methods through the Invoke method of the IDispatch interface, whereas a C++ program can access methods directly through the server’s vtable.

Fortunately Microsoft strongly recommends that anytime you build a COM server component and expose its interfaces, you do so as a dual interface. In fact, the MFC AppWizard and ClassWizard create components with dual interfaces by default.

A further development of common development languages such as Visual Basic and Java is that they both bind to an automation server’s vtable, thus eliminating much of the overhead involved in calling through automation interfaces. Today it is mainly scripting languages or scripting interpreters that interface with automation servers through the slower IDispatch interface (see Figure 12.8).


Figure 12.8  Dispatch interface and Invoke with methods and properties.



The Variant

One of the challenges that automation must deal with is the fact that, as with most COM object servers, there is no guarantee that the client and server reside either in the same process space or on the same machine. For that matter, both objects might reside on different machines with differing architectures. For example, an automation client (commonly referred to as a controller) can exist on an Intel-based computer, and the automation server can reside on an Alpha processor-based machine. The differences between these architectures can extend to native data lengths and byte ordering. An int on an Intel machine can be 32 bits long, whereas the corresponding Alpha representation can default to 64 bits. Also the high-byte/low-byte ordering can be opposite. Moving further away from such primitive types as these to more complex ones, such as strings and time/date representations, involves more processor time and data manipulation. The act of exchanging data and correctly reconstituting the data to a format native to the machine that is acting on it, is known as marshaling.

Automation employs a datatype known as the Variant to help exchange data values between client and server objects. The variant can be thought of as a union structure in C or C++. In fact, even better than thinking of it as being a union, it is actually represented internally as a union.

/* VARIANT STRUCTURE */
   VARTYPE vt;
   WORD wReserved1;
   WORD wReserved2;
   WORD wReserved3;
   union {
     LONG           VT_I4
     BYTE           VT_UI1
     SHORT          VT_I2
     FLOAT          VT_R4
     DOUBLE         VT_R8
     VARIANT_BOOL   VT_BOOL
     SCODE          VT_ERROR
     CY             VT_CY
     DATE           VT_DATE
     BSTR           VT_BSTR
     IUnknown *     VT_UNKNOWN
     IDispatch *    VT_DISPATCH
     SAFEARRAY *    VT_ARRAY
     BYTE *         VT_BYREF|VT_UI1
     SHORT *        VT_BYREF|VT_I2
     LONG *         VT_BYREF|VT_I4
     FLOAT *        VT_BYREF|VT_R4
     DOUBLE *       VT_BYREF|VT_R8
     VARIANT_BOOL * VT_BYREF|VT_BOOL
     SCODE *        VT_BYREF|VT_ERROR
     CY *           VT_BYREF|VT_CY
     DATE *         VT_BYREF|VT_DATE
     BSTR *         VT_BYREF|VT_BSTR
     IUnknown **    VT_BYREF|VT_UNKNOWN
     IDispatch **   VT_BYREF|VT_DISPATCH
     SAFEARRAY **   VT_BYREF|VT_ARRAY
     VARIANT *      VT_BYREF|VT_VARIANT
     PVOID          VT_BYREF (Generic ByRef)
     CHAR           VT_I1
     USHORT         VT_UI2
     ULONG          VT_UI4
     INT            VT_INT
     UINT           VT_UINT
     DECIMAL *      VT_BYREF|VT_DECIMAL
     CHAR *         VT_BYREF|VT_I1
     USHORT *       VT_BYREF|VT_UI2
     ULONG *        VT_BYREF|VT_UI4
     INT *          VT_BYREF|VT_INT
     UINT *         VT_BYREF|VT_UINT
   }

The OLE system DLLs provide all the code necessary to move the variant datatypes back and forth between controller and server. MFC offers a wrapper class for this structure, the COleVariant class, helping you to construct and interpret automation method arguments. Also, this class offers helper methods to convert between common datatypes.

As you work with unions, you will recognize the fact that you will sooner or later have to determine the data that the union actually stores. After all, a union doesn’t actually allocate the space for each of the datatypes; the compiler simply allocates space for the largest of the types, thus creating enough room for any of the smaller types as well. Identifying the data that the union contains is the job of the VARTYPE variable. Looking at the structure in the preceding code listing, you will notice a variable whose declaration is vt. You use (or your wrapper class uses) this variable to determine the data in the union. Table 12.2 gives the possible values of the vt variables.

Table 12.2 Values of vt Variables

Constant Value Description

VT_EMPTY 0 Not specified
VT_NULL 1 Null
VT_I2 2 2-byte signed int
VT_I4 3 4-byte signed int
VT_R4 4 4-byte real
VT_R8 5 8-byte real
VT_CY 6 Currency
VT_DATE 7 Date
VT_BSTR 8 Binary string
VT_DISPATCH 9 IDispatch
VT_ERROR 10 SCODES
VT_BOOL 11 Boolean TRUE=-1, FALSE=0
VT_VARIANT 12 VARIANT FAR*
VT_UNKNOWN 13 IUnknown FAR*
VT_UI1 17 Unsigned char

In addition to moving common datatypes back and forth, the standard OLE marshaler can exchange IUnknown interface pointers as well as IDispatch-derived interface pointers.

String representation is the BSTR datatype. Such a string is not NULL-terminated as is common in C or C++. Instead, the length of the string is prefixed to the beginning of the byte array.

An Automation Server Using MFC

Developing an automation server in MFC is not difficult—various wizards perform much of the work. Time to thank the wizards, again. Before you begin, here is a quick review of the steps necessary to produce an automation server:

  Select the automation object’s deliver vehicle—EXE or DLL.
  Define a dispinterface (an interface that derives from IDispatch).
  Add methods and properties to perform actions necessary by the server.
  Register the server and automation classes.

Server Type

As with ATL-based automation servers, MFC supports both executable (EXE) and dynamic link library (DLL) delivery vehicles for automation servers. When starting a new project, there are wizards named MFC AppWizard (dll) and MFC AppWizard (exe), which can be selected to house automation objects (see Figure 12.9). You will select an EXE server application for this example. The name of the project is autoserv.


Figure 12.9  Beginning the autoserv project.

The next step is to enable automation in the wizard-generated code. The steps that enable automation are different for the different server types. For DLLs (see Figure 12.10), it is step 1, and for EXEs (see Figure 12.11), it is step 3.


Figure 12.10  DLL automation option.


Figure 12.11  EXE automation option.



Upon completion of the AppWizard steps, both code and an .ODL will be generated. Your interface definition file will contain a single dispinterface definition and ClassWizard placeholders for properties and methods.

// autoserv.odl : type library source for autoserv.exe

// This file will be processed by the MIDL compiler to produce the
// type library (autoserv.tlb).

[ uuid(723B4DA4-B8AA-11D2-8FAF-00105A5D8D6C), version(1.0) ]
library Autoserv
{
    importlib(“stdole32.tlb”);
    importlib(“stdole2.tlb”);

    //  Primary dispatch interface for CAutoservDoc

    [ uuid(723B4DA5-B8AA-11D2-8FAF-00105A5D8D6C) ]
    dispinterface IAutoserv
    {
        properties:
       // NOTE - ClassWizard will maintain property information here.
            //    Use extreme caution when editing this section.
            //{{AFX_ODL_PROP(CAutoservDoc)
            //}}AFX_ODL_PROP
        methods:
         // NOTE - ClassWizard will maintain method information here.
            //    Use extreme caution when editing this section.
            //{{AFX_ODL_METHOD(CAutoservDoc)
            //}}AFX_ODL_METHOD

    };
    //  Class information for CAutoservDoc

    [ uuid(723B4DA3-B8AA-11D2-8FAF-00105A5D8D6C) ]
    coclass Document
    {
        [default] dispinterface IAutoserv;
    };
    //{{AFX_APPEND_ODL}}
    //}}AFX_APPEND_ODL}}
};

Adding methods and properties to your interface is simple using ClassWizard. In the ClassView tab of the project management pane in Visual C++, right-click the interface icon to display the context-sensitive menu for interface objects, as shown in Figure 12.12.


Figure 12.12  Context menu for interface definition.

Selecting the Add Method menu (refer to Figure 12.12) displays a dialog that you fill out to identify the parameters and return type for the method (see Figure 12.13). The name of the method is TrackInfo. It returns an SCODE, which is common for OLE calls, and it takes two arguments, a short named Index and a BSTR value named Title.


Figure 12.13  Adding a method.

Defining a property is just as easy. Select the Add Property menu (see Figure 12.12) on the interface object and complete the dialog. Figure 12.14 shows an AlbumLength property defined as a short value with a get and set function.


Figure 12.14  Adding a property.

ClassWizard inserts the following definitions into your .ODL file:

        properties:
       // NOTE - ClassWizard will maintain property information here.
            //    Use extreme caution when editing this section.
            //{{AFX_ODL_PROP(CAutoservDoc)
            [id(1)] short AlbumLength;
            //}}AFX_ODL_PROP

        methods:
         // NOTE - ClassWizard will maintain method information here.
            //    Use extreme caution when editing this section.
            //{{AFX_ODL_METHOD(CAutoservDoc)
            [id(2)] SCODE TrackInfo(short Index, BSTR* Title);
            //}}AFX_ODL_METHOD

The methods and properties are added to your project’s document object. Thus three methods appear on CAutoServDoc as a result of these operations: SetAlbumLength, GetAlbumLength, and TrackInfo.

Declaring and Defining Additional Dispinterfaces

MFC, as with ATL, allows a server to provide multiple automation class objects. EXE-based servers differ from DLL-based servers in that the AppWizard defines the first coclass. The preceding section describes how the ProgID is established in the Advanced dialog. In MFC, Automation classes must derive from CCmdTarget. If you look at an EXE-based server, you will find that the CDocument-derived class contains the automation property and method definitions. This is acceptable because CDocument derives from CCmdTarget.

To add automation class objects to a DLL, or add additional class objects to an EXE, you must use the New Class dialog. This dialog is invoked from the Insert, New Class menus. The dialog appears in Figure 12.15.


Figure 12.15  The New Class dialog.

The following review summarizes the steps or conditions that must be met to add another automation interface to a server:

  The class type must be MFC class.
  The class must be given a name.
  The base class must be CCmdTarget.
  An automation option must be selected.

By default the ProgID for the dispinterface is the name of the DLL or EXE project, followed by a period, followed by the name of the class. If you select automation, this is the ProgID that your interface receives. You can, however, select Createable by Type ID and supply a different ProgID.

In this case, the name of the class is SecondInterface, it correctly derives from CCmdTarget, and it can be created by the type ID, which is autoserv.SecondInterface.

Adding Methods and Properties

You might think that defining the interface for this new class is the same as defining the original interface: You right-click an interface in the ClassView pane of the Workspace window and select Add Method or Add Property. This is not entirely correct. If you view the ClassView tab, you will not find another interface with the name you expect; you do, however, find SecondInterface as a class.

To add properties and methods to your new class, you must display ClassWizard, select the Automation tab (see Figure 12.16), select the SecondInterface class in the Class Name combo box, and use the Add Method or Add Property functions.


Figure 12.16  The Automation tab of ClassWizard.

These buttons display the same methods and property editors that are discussed earlier.

It is interesting to note the OLE support code that ClassWizard adds to your new class to support the automation functionality, as follows:

/////////////////////////////////////////////////////////////////////
// SecondInterface
IMPLEMENT_DYNCREATE(SecondInterface, CCmdTarget)

SecondInterface::SecondInterface()
{
    EnableAutomation();

    // To keep the application running as long as an OLE automation
    //    object is active, the constructor calls AfxOleLockApp.

    AfxOleLockApp();
}

SecondInterface::-SecondInterface()
{
    // To terminate the application when all objects created with
    //     with OLE automation, the destructor calls AfxOleUnlockApp.

    AfxOleUnlockApp();
}

void SecondInterface::OnFinalRelease()
{
    // When the last reference for an automation object is released
    // OnFinalRelease is called.  The base class will automatically
    // deletes the object.  Add additional cleanup required for your
    // object before calling the base class.

    CCmdTarget::OnFinalRelease();
}

BEGIN_MESSAGE_MAP(SecondInterface, CCmdTarget)
    //{{AFX_MSG_MAP(SecondInterface)
   // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

BEGIN_DISPATCH_MAP(SecondInterface, CCmdTarget)
    //{{AFX_DISPATCH_MAP(SecondInterface)
   // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

// Note: you add support for IID_ISecondInterface to support
// typesafe binding
//  from VBA.  This IID must match the GUID that is attached to the
//  dispinterface in the .ODL file.

// {723B4DBA-B8AA-11D2-8FAF-00105A5D8D6C}
static const IID IID_ISecondInterface =
{ 0x723b4dba, 0xb8aa, 0x11d2, { 0x8f, 0xaf, 0x0, 0x10, 0x5a, 0x5d,\
 0x8d, 0x6c } };

BEGIN_INTERFACE_MAP(SecondInterface, CCmdTarget)
    INTERFACE_PART(SecondInterface, IID_ISecondInterface, Dispatch)
END_INTERFACE_MAP()

// {723B4DBB-B8AA-11D2-8FAF-00105A5D8D6C}
IMPLEMENT_OLECREATE(SecondInterface, “autoserv.SecondInterface”, \
 0x723b4dbb, 0xb8aa, 0x11d2, 0x8f, 0xaf, 0x0, 0x10, 0x5a, 0x5d, \
0x8d, 0x6c)

//////////////////////////////////////////////////////////////// SecondInterface message handlers

The constructor for SecondInterface calls AfxOleLockApp, keeping this server in memory during use, and calls AfxOleUnlockApp in the destructor, allowing the server to release as necessary. The constructor also makes a call to EnableAutomation. This call is also made in InitInstance for this application, but if an automation-enabled class was added to a project that didn’t currently support such operations, this call would cover your bases. Additionally, a template copy of OnFinalRelease is offered, calling the CCmdTarget base class. Prior to releasing the server, this method is called, allowing you to perform additional cleanup.

Summary

In this chapter you have explored the server technologies that the MFC OLE libraries support. While the problems that are solved by document servers and automation servers differ vastly, you have seen that the concept of interface programming by COM is easily leveraged by both.

You will exercise the automation server interfaces in the next chapter, “MFC OLE Clients.” You will see that there are a number of ways to access dispinterfaces from an MFC client application.